iT邦幫忙

2024 iThome 鐵人賽

DAY 19
1
佛心分享-SideProject30

用 Golang 實作 streamlit 系列 第 19

Day19 StatefulWebsocket

  • 分享至 

  • xImage
  •  

Day18 提到了 Client 端在進入頁面時,會開一個 websocket,然後在接下來都用它來和後端溝通。

這並不是件簡單的事情,因為網路連線有可能會有各種問題, Client 要好好處理,例如網路環境的不穩定性可能會導致連線中斷,這時就需要一個健壯的狀態機來管理連線狀態。

WebSocket 狀態機會根據不同的網路狀況,將連線狀態切換到不同的階段,我們可以先做簡單的四個狀態:

  • 初始化 (Init):剛進入頁面時,狀態機處於初始化狀態。
  • 心跳 (Ping):定期向後端發送心跳包,確認後端是否在線。
  • 嘗試連線 (TryConn):嘗試建立 WebSocket 連線。
  • 連線中 (Conn):WebSocket 連線已建立,可以進行正常的資料傳輸。

所以狀態機大致上可以畫成這樣:

準備 Enum 對應狀態和路徑:

enum WebSocketState {
  Initial,
  Ping,
  TryConnect,
  Connected,
}

enum WebSocketAction {
  OK,
  Error,
  Closed,
}

我們可以把 StatefulWebsocket 打包起來,讓使用者決定在各個 Action 時需要做甚麼事情,例如可以在斷開連線時在頁面顯示連線中。

export class StatefulWebSocket {
  conn: WebSocket | null
  state: WebSocketState
  pageName: string
  stateID: string

  /** Receive pack from connected websocket. */
  recv: (pack: any) => void

  /** Call when stateID is assigned a new value from server. */
  onStateIDChange: () => void = () => { }

  /** Call when state change from TryConnect to Connected. */
  onConnect: () => void = () => { }

  constructor(pageName: string, recv: (pack: any) => void) {
    this.state = WebSocketState.Initial
    this.pageName = pageName
    this.conn = null
    this.stateID = ''
    this.recv = recv
  }
  // ...
}

一開始時進入 Init 狀態

init() {
  if (this.state !== WebSocketState.Initial) {
    throw new Error('init should call on state Initial')
  }

  this.walk(WebSocketAction.OK)
}

Ping 的時候會不斷的去確認 server 是否可以戳到:

async ping() {
  var waitMS = 200

  while (1) {
    var resp: Response
    try {
      resp = await fetch(healthCheckURL)
      if (resp.status === 200) {
        break
      }

      console.error('health check', resp)
    } catch (e) {
      console.error(e)
    }

    await sleep(waitMS)

    waitMS *= 1.5
    waitMS = Math.min(waitMS, 60000)
  }

  console.log('ping ok')
  this.walk(WebSocketAction.OK)
}

連線

tryConnect() {
  this.conn = new WebSocket(getUpdateURI(this.pageName))
  var that = this

  this.conn.onopen = function () {
    that.conn.send(JSON.stringify({ state_id: that.stateID }))
    console.log('socket open ok')
    that.walk(WebSocketAction.OK)
  }

  this.conn.onmessage = function (e) {
    const data = JSON.parse(e.data)
    if (data.state_id) {
      that.stateID = data.state_id
      that.onStateIDChange()
      return
    }

    that.recv(data)
  }

  this.conn.onclose = function () {
    that.conn = null
    that.walk(WebSocketAction.Closed)
  }
}

狀態轉移:

walkTo(state: WebSocketState) {
  switch (state) {
    case WebSocketState.Ping:
      this.state = state
      this.ping()
      break
    case WebSocketState.TryConnect:
      this.state = state
      this.tryConnect()
      break
    case WebSocketState.Connected:
      this.state = state
      this.onConnect()
      break
    default:
      console.error('undefined state', state)
      throw new Error('undefine state')
  }
}
  
walk(action: WebSocketAction) {
  switch (this.state) {
    case WebSocketState.Initial:
      if (action === WebSocketAction.OK) {
        this.walkTo(WebSocketState.Ping)
        return
      }
      break
    case WebSocketState.Ping:
      if (action == WebSocketAction.OK) {
        this.walkTo(WebSocketState.TryConnect)
        return
      }
      break
    case WebSocketState.TryConnect:
      switch (action) {
        case WebSocketAction.OK:
          this.walkTo(WebSocketState.Connected)
          return
        case WebSocketAction.Error:
        case WebSocketAction.Closed:
          this.walkTo(WebSocketState.Ping)
          return
        default:
          break
      }
      break
    case WebSocketState.Connected:
      switch (action) {
        case WebSocketAction.Error:
        case WebSocketAction.Closed:
          this.walkTo(WebSocketState.Ping)
          return
        default:
          break
      }
      break
  }
  
  console.error('undefine action on state', this.state, action)
  throw new Error('undefine action on state')
}

最後提供一個從 websocket 送資料的介面

send(pack: any) {
  if (this.state !== WebSocketState.Connected || this.conn === null) {
    throw new Error('websocket is not prepared')
  }

  this.conn.send(JSON.stringify(pack))
}

通過建立一個狀態機,我們可以有效地管理 WebSocket 連線,讓 App 前端更穩定。


上一篇
Day18: Delta Update: WebSocket
下一篇
Day20 ProgressBar
系列文
用 Golang 實作 streamlit 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言